﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Net;
using System.Diagnostics;

namespace BitcoinLibrary
{
    /**
* A NetworkConnection handles talking to a remote BitCoin peer at a low level. It understands how to read and write
* messages off the network, but doesn't asynchronously communicate with the peer or handle the higher level details
* of the protocol. After constructing a NetworkConnection, use a {@link Peer} to hand off communication to a
* background thread.
*
* Construction is blocking whilst the protocol version is negotiated.
*/
    public class NetworkConnection
    {
        private readonly TcpClient socket;
        private readonly Stream outStream;
        private readonly Stream inStream;
        // The IP address to which we are connecting.
        private readonly IPAddress remoteIp;
        private readonly NetworkParameters param;
        private readonly VersionMessage versionMessage;
        private const bool PROTOCOL_LOG = false;

        private BitcoinSerializer serializer = null;

        /**
        * Connect to the given IP address using the port specified as part of the network parameters. Once construction
        * is complete a functioning network channel is set up and running.
        *
        * @param remoteIp IP address to connect to. IPv6 is not currently supported by BitCoin.
        * @param params Defines which network to connect to and details of the protocol.
        * @param bestHeight How many blocks are in our best chain
        * @param connectTimeout Timeout in milliseconds when initially connecting to peer
        * @throws IOException if there is a network related failure.
        * @throws ProtocolException if the version negotiation failed.
        */
        public NetworkConnection(IPAddress remoteIp, NetworkParameters param, int bestHeight, int connectTimeout)
        {
            this.param = param;
            this.remoteIp = remoteIp;

            IPEndPoint address = new IPEndPoint(remoteIp, param.port);
            socket = new TcpClient();
            socket.Connect(address/*, connectTimeout*/);

            outStream = socket.GetStream();
            inStream = socket.GetStream();

            // the version message never uses checksumming. Update checkumming property after version is read.
            this.serializer = new BitcoinSerializer(param, false);

            // Announce ourselves. This has to come first to connect to clients beyond v0.30.20.2 which wait to hear
            // from us until they send their version message back.
            writeMessage(new VersionMessage(param, bestHeight));
            // When connecting, the remote peer sends us a version message with various bits of
            // useful data in it. We need to know the peer protocol version before we can talk to it.
            versionMessage = (VersionMessage)readMessage();
            // Now it's our turn ...
            // Send an ACK message stating we accept the peers protocol version.
            writeMessage(new VersionAck());
            // And get one back ...
            readMessage();
            // Switch to the new protocol version.
            int peerVersion = versionMessage.clientVersion;
            Debug.WriteLine("Connected to peer: version={}, subVer='{}', services=0x{}, time={}, blocks={}", new Object[] {
peerVersion, 
versionMessage.subVer,
versionMessage.localServices, 
new DateTime(versionMessage.time * 1000),   // TODO: fix
versionMessage.bestHeight
});
            // BitCoinJ is a client mode implementation. That means there's not much point in us talking to other client
            // mode nodes because we can't download the data from them we need to find/verify transactions.
            if (!versionMessage.hasBlockChain())
                throw new ProtocolException("Peer does not have a copy of the block chain.");
            // newer clients use checksumming
            serializer.useChecksumming(peerVersion >= 209);
            // Handshake is done!
        }

        /**
        * Sends a "ping" message to the remote node. The protocol doesn't presently use this feature much.
        * @throws IOException
        */
        public void ping()
        {
            writeMessage(new Ping());
        }

        /**
        * Shuts down the network socket. Note that there's no way to wait for a socket to be fully flushed out to the
        * wire, so if you call this immediately after sending a message it might not get sent.
        */
        public void shutdown()
        {
            //socket.shutdownOutput();
            //socket.shutdownInput();
            socket.Close();
        }

        public override string ToString()
        {
            return "[" + remoteIp.ToString() + "]:" + param.port + " (" + (socket.Connected ? "connected" :
            "disconnected") + ")";
        }

        /**
        * Reads a network message from the wire, blocking until the message is fully received.
        *
        * @return An instance of a Message subclass
        * @throws ProtocolException if the message is badly formatted, failed checksum or there was a TCP failure.
        */
        public Message readMessage()
        {
            return serializer.deserialize(inStream);
        }

        /**
        * Writes the given message out over the network using the protocol tag. For a Transaction
        * this should be "tx" for example. It's safe to call this from multiple threads simultaneously,
        * the actual writing will be serialized.
        *
        * @throws IOException
        */
        public void writeMessage(Message message)
        {
            lock (outStream)
            {
                serializer.serialize(message, outStream);
            }
        }

        /** Returns the version message received from the other end of the connection during the handshake. */
        public VersionMessage getVersionMessage()
        {
            return versionMessage;
        }
    }
}
